import collections
import cStringIO
import inspect
import logging
import os
import pprint
import random
import re

from wmh import testing

import parser
import parser_test
import python2 as python

class TestCase(testing.TestCase):

  def setUp(self):
    super(TestCase, self).setUp()
    self._metac = parser.Compiler(metal='oopl', basel='python')
    self._module = python.Module.New('./testdata/kernel.py')
    self._class = self._module.classNamed('A')
    self._method = self._class.methodNamed('instance')
    self._method2 = self._class.methodNamed('Static')
    self._method3 = self._class.methodNamed('Meta')

    # maps path to python.Module instances
    self._modules = {}

  def parseModule(self, filepath):
    modules = self._modules
    result = modules.get(filepath, None)
    if not result:
      result = python.Module.New(filepath)
      modules[filepath] = result
    return result

  def parseGivenClass(self, dotname):
    parts = dotname.split('.')
    clsname = parts.pop()
    path = './' + '/'.join(parts) + '.py'
    module = self.parseModule(path)
    return module.classNamed(clsname)

  def convertGivenClass(self, dotname):
    cls = self.parseGivenClass(dotname)
    output = python.Output()
    cls.toMeta(self._metac, output, indent='  ')
    return '\n'.join(output.lines()) + '\n'

  def parseGivenMethod(self, dotname):
    parts = dotname.split('.')
    methname = parts.pop()
    cls = self.parseGivenClass('.'.join(parts))
    return cls.methodNamed(methname)

  def convertGivenMethod(self, dotname):
    method = self.parseGivenMethod(dotname)
    output = python.Output(indent='    ')
    method.toMeta(self._metac, output)
    return '\n'.join(output.lines()) + '\n'

  def ParseModule(self, path='./testdata/kernel.py'):
    """Create a Module instance (which recursively creates Region instances."""
    del python.Module.MODULES[path]
    self._module = python.Module.New(path)
    self._module_region = self._module.source()
    self._module_test_region = self._module.testSources()[0]
    self._file = self._module_region.file()
    return self._module

  def ParseKernel(self):
    """Parse testdata/kernel.py and extract useful objects."""
    self.assertEqual('kernel', self._region1.name())
    self._region_kernel = self._region1
    self._region_kernel.split('class', '')
    self._region_A = self._region_kernel.child('A')
    self._region_A.split('def', '  ')
    self._region_SubClass = self._region_kernel.child('SubClass')
    self._region_SubClass.split('def', '  ')
    self._region_createData = self._region_SubClass.child('createData')


class FileTest(TestCase):

  def setUp(self):
    super(FileTest, self).setUp()
    self._file = python.File('testdata/kernel.py')
    #self._testfile = python.File('testdata/kernel_test.py')

  def test___init__(self):
    fle = self._file

    self.assertEqual(
      [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 22, 23, 24, 25, 26, 27, 28, 29],
      fle._line_map[:20])

    s = """
    single simple single: 'hello world'.
    single complex single: 'hello \\'there\\' world'.
    single simple double: "hello world".
    single complex double: "hello \\"there\\" world".

    double simple single: 'hello world' or 'bye now'.
    double complex single: 'hello \\'there\\' world' or 'bye \\'here\\' now'.
    double simple double: "hello world" or "bye now".
    double complex double: "hello \\"there\\" world" or "bye \\"here\\" now".
    """

    self.assertMultiLineEqual("""
    single simple single: <@s@>.
    single complex single: <@s@>.
    single simple double: "hello world".
    single complex double: "hello \\"there\\" world".

    double simple single: <@s@> or <@s@>.
    double complex single: <@s@> or <@s@>.
    double simple double: "hello world" or "bye now".
    double complex double: "hello \\"there\\" world" or "bye \\"here\\" now".
    """, python.File.SINGLELINE_SGLSTR_RE.sub('<@s@>', s))

    self.assertMultiLineEqual("""
    single simple single: 'hello world'.
    single complex single: 'hello \\'there\\' world'.
    single simple double: <@s@>.
    single complex double: <@s@>.

    double simple single: 'hello world' or 'bye now'.
    double complex single: 'hello \\'there\\' world' or 'bye \\'here\\' now'.
    double simple double: <@s@> or <@s@>.
    double complex double: <@s@> or <@s@>.
    """, python.File.SINGLELINE_DBLSTR_RE.sub('<@s@>', s))

    self.assertMultiLineEqual("""
    single simple single: <@s@>.
    single complex single: <@s@>.
    single simple double: <@s@>.
    single complex double: <@s@>.

    double simple single: <@s@> or <@s@>.
    double complex single: <@s@> or <@s@>.
    double simple double: <@s@> or <@s@>.
    double complex double: <@s@> or <@s@>.
    """,
    python.File.SINGLELINE_SGLSTR_RE.sub(
      '<@s@>', python.File.SINGLELINE_DBLSTR_RE.sub('<@s@>', s)))

  def test_verify(self):
    file1 = self._file
    file1.verify()
    file2 = python.File('parser.py')
    file2.verify()

  def test_srcSize(self):
    self.assertEqual(3272, self._file.srcSize())

  def test_sourceSize(self):
    self.assertEqual(10540, self._file.sourceSize())

  def test_src(self):
    self.assertMultiLineEqual(
      '<@S001@>\n'
      '\n'
      'import os\n',
      self._file.src(0, 20))

    self.assertMultiLineEqual(
      '  def text(self):\n'
      '    <@S003@>\n'
      '    return self.strval().strip()\n'
      '\n'
      '  def __str__(self):\n'
      '    <@S004@>\n'
      "    indent = <@s002@>\n"
      "    return <@s003@> % (self.intval(), <@s004@> * indent, "
      "self.strval())",
      self._file.src(546, 738))

  def test_source(self):
    self.assertMultiLineEqual(
      '"""Module documentation.\n'
      '\n'
      'The first docstr of the file.\n'
      '"""\n'
      '\n'
      'import os\n',
      self._file.source(0, 20))

    self.assertMultiLineEqual(
      '  def text(self):\n'
      '    """The line with leading/trailing whitespace (including newline) '
      'removed."""\n'
      '    return self.strval().strip()\n'
      '\n'
      '  def __str__(self):\n'
      '    """Provide string representation of this instance."""\n'
      "    indent = '   '\n"
      "    return '%4d: %s%s\\n' % (self.intval(), ' ' * indent, "
      "self.strval())",
      self._file.source(546, 738))

    # This verifies that replacing all strings in file._src_str gives us back
    # file._source_str.
    self.assertMultiLineEqual(
      self._file._source_str,
      self._file.source(0, len(self._file._src_str)))

  def test_srcToSource(self):
    pass

  def test_numToNumber(self):
    f = self._file
    start = 0
    end = f.srcSize()
    src_lines = f.src(start, end).split('\n')
    src_lines.insert(0, '')
    source_lines = f.source(start, end).split('\n')
    source_lines.insert(0, '')
    lmap = f._line_map

    ignored = 0
    for i in range(0, 100):
      src_lineno = random.randint(1, len(src_lines) - 1)
      try:
        source_lineno = f.numToNumber(src_lineno)
      except IndexError as e:
        self.assertTrue(False, 'index error for src lineno %d' % src_lineno)
      src_line = src_lines[src_lineno]
      if python.File.HOLDER_RE.search(src_line):
        # These two lines will differ because one is a string holder while the
        # other is the string value.  We ignore them under the assumption
        # that if 100 randomly picked lines without string literals match,
        # we've gotten things right.
        ignored += 1
      else:
        if src_line.find('<@') > -1:
          m = python.File.HOLDER_RE.search(src_line)
        self.assertEqual(
          src_line, source_lines[source_lineno],
          'src %d != %d source: %s' % (src_lineno, source_lineno, src_line))

  def test_keyToStr(self):
    pass


class RegionTest(TestCase):

  def setUp(self):
    super(RegionTest, self).setUp()

    self._file = python.File('testdata/kernel.py')
    self._srcsize = self._file.srcSize()
    self._testfile = python.File('testdata/kernel_test.py')

    self._region1 = python.Region(self._file, 'module', 'kernel')

  def test___init__(self):
    fileobj = python.File('testdata/tiny.py')
    modreg = python.Region(fileobj, 'module', 'tiny')
    modsrc = modreg.src()
    sre = python.Region.GenerateRegexp('class', '', '  ')
    matches = list(sre.finditer(modsrc))
    self.assertEqual(
      {'comment': '<@S001@>',
       'comment_tail': '',
       'indent': '  ',
       'keyword': 'class',
       'name': 'Person',
       'oneline': None,
       'params': 'object',
       'scope': '\n\n  VAR1 = 10\n\n  def __init__(self):\n    pass\n\n  def instance(self):\n    a = 1\n    b = 2\n\n    c = 3\n\n  d = 4\n\n  @staticmethod\n  def static():\n    pass\n\n  VAR2 = {\n    <@s001@>: 1,\n    <@s002@>: 2,\n  }\n\n  @classmethod\n  def cls():\n    pass\n\n  VAR3 = <@s003@>\n\n\n'},
      matches[0].groupdict())
    self.assertEqual(
      {'comment': None,
       'comment_tail': 'pass',
       'indent': '  ',
       'keyword': 'class',
       'name': 'Student',
       'oneline': None,
       'params': 'Person',
       'scope': '\n\n\n'},
      matches[1].groupdict())

  def test_file(self):
    self.assertTrue(self._region1._file is self._region1.file())

  def test_kind(self):
    self.assertEqual('module', self._region1.kind())

  def test_name(self):
    self.assertEqual('kernel', self._region1.name())

  def test_start(self):
    self.assertEqual(0, self._region1.start())

  def test_end(self):
    self.assertEqual(self._file.srcSize(), self._region1.end())

  def test_child(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertEqual('createData', clsreg.child('createData').name())
    self.assertEqual(None, clsreg.child('not-a-child'))

  def test_children(self):
    self.assertTrue(self._region1._children is self._region1.children())

  def test_GenerateRegexp(self):
    class_re = python.Region.GenerateRegexp('class', '', '  ')
    class_match = class_re.search(
      'class A(object):\n'
      '  <@S001@>\n'
      '\n'
      '  def __init__(self):\n'
      '    pass\n')
    self.assertEqual(
      {'comment': '<@S001@>',
       'indent': '  ',
       'name': 'A',
       'oneline': None,
       'keyword': 'class',
       'params': 'object',
       'scope': '\n\n  def __init__(self):\n    pass\n',
       'comment_tail': ''},
      class_match.groupdict())

    class_match = class_re.search(
      'class B(A):\n'
      '  pass\n')
    self.assertEqual(
      {'comment': None,
       'indent': '  ',
       'name': 'B',
       'oneline': None,
       'keyword': 'class',
       'params': 'A',
       'scope': '\n',
       'comment_tail': 'pass'},
      class_match.groupdict())

    method_re = python.Region.GenerateRegexp('def', '  ', '    ')
    method_match = method_re.search(
      'class A(object):\n'
      '  def func(self, a, b=2):\n'
      '    <@S002@>\n'
      '    return a * b\n')
    self.assertEqual(
      {'comment': '<@S002@>',
       'indent': '    ',
       'name': 'func',
       'oneline': None,
       'keyword': 'def',
       'params': 'self, a, b=2',
       'scope': '\n    return a * b\n',
       'comment_tail': ''},
      method_match.groupdict())

    method_match = method_re.search(
      '\n'
      '  def data(self): return self._data\n'
      '  def dataIs(self, data): self._data = data\n')
    self.assertEqual(
      {'comment': None,
       'indent': None,
       'name': 'data',
       'keyword': 'def',
       'params': 'self',
       'oneline': 'return self._data',
       'scope': None,
       'comment_tail': None},
      method_match.groupdict())

  def test_src(self):
    self.assertEqual(
      self._file.src(0, self._srcsize),
      self._region1.src())

    self.assertMultiLineEqual(
      'def Main():\n'
      '  print <@s012@>\n'
      '  a = B()\n',
      self._file.src(3197, 3236))

  def test_source(self):
    self.assertMultiLineEqual(
      'def Main():\n'
      "  print 'Hello World'\n"
      '  a = B()\n',
      self._file.source(3197, 3236))

  def test__substr(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertEqual(
      '# Anything at the same level as the class just before\n'
      '# it is considered preamble.\n',
      clsreg._substr('pre', 'start', sub=True))

    # method2 is A.Static, which has a multi-line literal string within its
    # scope that needs to be properly indented and escaped with |>
    method = self._method2
    region = method.source()
    print region.body(sub=False, indent='  ')

  def test_preamble(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertEqual(
      '# Anything at the same level as the class just before\n'
      '# it is considered preamble.\n',
      clsreg.preamble(sub=True))
    self.assertEqual(
      '# Anything at the same level as the class just before\n'
      '# it is considered preamble.\n',
      clsreg.preamble(sub=False))

  def test_postamble(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertMultiLineEqual(
      '\n'
      '# Postamble for SubClass\n'
      '\n',
      clsreg.postamble(sub=True))
    self.assertMultiLineEqual(
      '\n'
      '# Postamble for SubClass\n'
      '\n',
      clsreg.postamble(sub=False))

  def test_argamble(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertEqual('(BaseClass)', clsreg.argamble(sub=True))
    self.assertEqual('(BaseClass)', clsreg.argamble(sub=False))

  def test_docstr(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertMultiLineEqual(
      '\"\"\"Class documentation.\n'
      '\n'
      'Attributes:\n'
      '  data: dict\n'
      '\"\"\"',
      clsreg.docstr())

    # TODO(wmh): The first line of the docstr is not being indented when
    # indentation is requested.  Should the 'comment' position start on
    # the newline before the docstr?  Or should we handle docstrings
    # entirely different, since we know the string replacement?
    self.assertMultiLineEqual(
      '\"\"\"Class documentation.\n'
      '\n'
      '  Attributes:\n'
      '    data: dict\n'
      '  \"\"\"',
      clsreg.docstr(indent=True))

  def test_body(self):
    self.ParseKernel()
    clsreg = self._region_SubClass
    self.assertMultiLineEqual(
      '\n'
      '\n'
      'def __init__(self, strval, intval=10, data=None):\n'
      '  super(SubClass, self).__init__(strval, intval)\n'
      '  self._data = data\n'
      '\n'
      'def createData(self, a, b, c):\n'
      '  data = {<@s005@>: a, <@s006@>: b, <@s007@>: c}\n'
      '  self._data = data\n'
      '  return data\n',
      clsreg.body(sub=True))

    methreg = self._region_createData
    self.assertMultiLineEqual(
      '\n'
      'data = {<@s005@>: a, <@s006@>: b, <@s007@>: c}\n'
      'self._data = data\n'
      'return data\n',
      methreg.body(sub=True))
    self.assertMultiLineEqual(
      '\n'
      "    data = {'a': a, 'b': b, 'c': c}\n"
      '    self._data = data\n'
      '    return data\n',
      methreg.body(sub=False, indent=True))

  def test_entityPreamble(self):
    pass

  def test_split(self):
    self.ParseKernel()
    self.assertEqual(
      ['BaseClass', 'SubClass', 'A', 'B', 'C', 'FileParser'],
      self._region_kernel.children().keys())

    self.assertEqual({
      'pre': 740,
      'start': 823,
      'sparams': 837,
      'eparams': 848,
      'comment': 852,
      'scope': 860,
      'post': 1112,
      'end': 1139,
      }, self._region_SubClass._positions)

    self.assertEqual(
      ['__init__', 'delim', 'delimIs', 'data', 'dataIs', 'dataRef', 'instance',
       'Static', 'test', 'Meta'],
      self._region_A.children().keys())

    self.assertEqual({
      'pre': 990,
      'start': 990,
      'sparams': 1006,
      'eparams': 1021,
      'comment': -1,
      'scope': 1022,
      'post': 1112,
      'end': 1139,
      }, self._region_createData._positions)

  def test_debug(self):
    pass

  #def test_it(self):
  #  module = self.ParseModule()
  #  cls1 = module.classNamed('SubClass')
  #  cls1.source().debug()
  #  print '#' * 70
  #  cls2 = module.classNamed('A')
  #  cls2.source().debug()
  #  #module.summarize()


class ModuleTest(TestCase):

  def test_New(self):
    module = python.Module.New('./testdata/kernel.py')
    #module.summarize()
    #module2 = python.Module.New('./python.py')
    #module2.summarize()
    module3 = python.Module.New('./parser.py')
    #module3.summarize()
    classes = module3._classes
    class_count = len(classes)
    method_count = 0
    for name, clazz in classes.iteritems():
      method_count += len(clazz._methods)
    #print 'Parsed %d methods in %d classes' % (method_count, class_count)
    self.assertTrue(method_count > 290 and method_count < 310)
    self.assertEqual(48, class_count)

  def test__Load(self):
    path = './testdata/kernel.py'
    basename, source, test_source = python.Module._Load(path)
    self.assertEqual('kernel', basename)
    self.assertTrue('class BaseClass(object):' in source.source())
    self.assertTrue('class BaseClassTest(TestCase):' in test_source.source())

  def test___init__(self):
    self.assertEqual('./testdata/kernel.py', self._module._path)
    self.assertEqual(6, len(self._module._classes))

  def test_path(self):
    # noop
    pass

  def test_sourceText(self):
    self.assertTrue(
        'The first docstr of the file.' in self._module.sourceText())

  def test_classNamed(self):
    self.assertEqual('A', self._module.classNamed('A').name())
    self.assertEqual(None, self._module.classNamed('NotAClass'))

  def test_summarize(self):
    fp = cStringIO.StringIO()
    self._module.summarize(fp=fp)
    self.assertTrue(
      ': delimIs                                  |' in fp.getvalue())

  def test_toMeta(self):
    output = python.Output()
    module = self._module
    self._module.toMeta(self._metac, output, indent='')

    filename = 'tmp/partial_parser2.meta'
    with open(filename, 'w') as fp:
      fp.write('\n'.join(output.lines()))
    print 'Do something with ModuleTest.test_toMeta (%s)' % filename

  def test_toMeta2(self):
    output = python.Output()    
    module = self.ParseModule()
    module.toMeta(self._metac, output, indent='')


class ClassTest(TestCase):

  def test___init__(self):
    self.assertEqual(
      ['__init__', 'delim', 'delimIs', 'data', 'dataIs', 'dataRef',
       'instance', 'Static', 'test', 'Meta'],
      self._class._methods.keys())

  def test_fieldMap(self):
    self.assertEqual(
      collections.OrderedDict(
        [('delim', {
          'field': '_delim',
          'value': 'delim',
          'type': 'str',
          'name':
          'delim',
          'comments': [
            'The delimiter between list elements.']}),
         ('data', {
           'field': '_data',
           'value': 'data',
           'type': 'list of str',
           'name': 'data',
           'comments': [
             'The elements of the list.']})]),
      self._class.fieldMap())

  def test_show(self):
    fp = cStringIO.StringIO()
    self._class.show(fp=fp)
    self.assertTrue('def Meta(cls, a):' in fp.getvalue())
    # TODO(wmh): More testing here.

  def test_summarize(self):
    fp = cStringIO.StringIO()
    self._class.summarize(fp=fp)
    self.assertTrue(
      ': data                                     | \n' in fp.getvalue())
    #print fp.getvalue()
    # TODO(wmh): Test full value of fp when kernel.py is more stable.

  def test_methodNamed(self):
    self.assertEqual('instance', self._class.methodNamed('instance').name())
    self.assertEqual(None, self._class.methodNamed('missing'))

  def test_methodsStartingWith(self):
    cls = self._module.classNamed('BaseClass')
    self.assertEqual(
      ['strval', 'strvalIs', 'strvalRef'],
      [m.name() for m in cls.methodsStartingWith('str')])

  def test_toMeta(self):
    output = python.Output()
    output.addPrimary('namespace', 'kernel.parser')

    classes_output = output.clone(delta_indent='  ')
    for clsname in (
        'parser.Logger', 'parser.Meta', 'parser.LiteralList', 'parser.Type'):
      cls = self.parseGivenClass(clsname)
      classes_output.reset()
      classes_output.newline()
      cls.toMeta(self._metac, classes_output, indent='  ')

    output.addBlock('scope', classes_output.lines())
    output.newline()
    output.addTerm('namespace', 'kernel.parser')

    filename = 'tmp/partial_parser.meta'
    with open(filename, 'w') as fp:
      fp.write('\n'.join(output.lines()))
    
    print 'Do something with ClassTest.test_toMeta (%s)' % filename
    #print self.convertGivenClass('parser.LiteralList')


class MethodTest(TestCase):

  def test___init__(self):
    self.assertEqual(None, self._module._parent)
    self.assertTrue(self._module is self._class._parent)

  def test_superInfo(self):
    self.assertEqual(
        {'text': 'super(A, self).__init__()',
         'args': '',
         'inheritance': 'post_extend'},
      self._method.superInfo())
    self.assertEqual(None, self._method2.superInfo())
    method4 = self._class.methodNamed('test')
    self.assertEqual(
      {'text': 'super(A, self).test(a, b, c)',
       'args': 'a, b, c',
       'inheritance': 'pre_extend'},
      method4.superInfo())

  def test_level(self):
    self.assertEqual('instance', self._method.level())
    self.assertEqual('static', self._method2.level())
    self.assertEqual('meta', self._method3.level())

  def test_comments(self):
    self.assertEqual(
      {'args': {
        'first_positional_argument': {
          'type': 'int',
          'name': 'first_positional_argument',
          'comments': ['A simple int arg.']},
        'second_positional_argument': {
          'type': 'str',
          'name': 'second_positional_argument',
          'comments': [
            'A simple str arg. This docstr intentionally goes on and on so that we',
            'can test multi-line docs.']
          },
        'first_keyword_argument': {
          'type': 'int',
          'name': 'first_keyword_argument',
          'comments': [
            'A keyword arg. This docstr has',
            '  indented text', 'so we can verify it is preserved.']
          }
        },
       'comments': ['Instance method.']},
      self._method.comments())
    
    self.assertEqual(
      {'args': {
        'a': {
          'type': 'int',
          'name': 'a',
          'comments': ['A simple int arg.']
          }
        },
       'comments': ['Static method.']},
      self._method2.comments())

    self.assertEqual(
      {'comments': ['Class method with no arg docs.']},
      self._method3.comments())

  def test__parseComment(self):
    self.maxDiff = None
    self.assertEqual(
      {'args': {
        'first_positional_argument': {
          'type': 'int',
          'name': 'first_positional_argument',
          'comments': ['A simple int arg.']},
        'second_positional_argument': {
          'type': 'str',
          'name': 'second_positional_argument',
          'comments': [
            'A simple str arg. This docstr intentionally goes on and on so '
            'that we',
            'can test multi-line docs.']},
        'first_keyword_argument': {
          'type': 'int',
          'name': 'first_keyword_argument',
          'comments': [
            'A keyword arg. This docstr has',
            '  indented text',
            'so we can verify it is preserved.']}},
       'comments': ['Instance method.']},
       self._method._parseComment())

    class_ = self._module.classNamed('FileParser')
    method = class_.methodNamed('parseAttribute')
    self.assertEqual(
      {'args': {
        'construct': {
          'comments': [
            'The construct for which the attribute must be legal.'],
          'name': 'construct',
          'type': 'Construct'},
        'is_primary': {
          'comments': [
            'True if we are parsing a primary attribute, false if we are parsing',
            'a secondary attribute. There is some correlation between keyinfo and',
            'is_primary, but they are separate for added flexibility.'],
          'name': 'is_primary',
          'type': 'bool'},
        'keyinfo': {
          'comments': [
            'Usually, this is None, but if it is specified, it means the key portion',
            'of the attribute has already been parsed (and self._pos is past this',
            'key) so that we only need to parse the value.  If not specified, we are',
            'to also parse the attribute key itself.'],
          'name': 'keyinfo',
          'type': 'three-tuple or None'},
        'required_indent': {
          'comments': [
            'The required indentation level of the line that starts the attribute.',
            'An error is reported if this invariant is not maintained.'],
          'name': 'required_indent',
          'type': 'int'}},
       'comments': [
         'Parse an attribute key/value pair for the specified construct.',
         '',
         'Side Effects:',
         ' - self.pos() is advanced to the character after the attribute value.  If',
         "   the terminator (';') is encountered, self.pos() stays on that char.",
         '',
         'An attribute consists of:',
         ' - an attribute key or key abbrev.  Keys are always ids, but abbrevs can',
         '   be operators or ids or other words.',
         ' - an optional attribute parameter',
         ' - an attribute value:',
         '    - a literal int    i.e. 42            (arbitrary precision ... code uses appropriate datastructure to hold it)',
         '    - a literal float  i.e. 3.1415926535  (arbitrary precision ... code uses appropriate datastructure to hold it)',
         "    - a literal string i.e. 'this is a test'",
         '    - a literal list   i.e. List|<a|b|c>',
         "    - a word           i.e. '_39+.@$a'    (anything except whitespace)",
         "    - an identifier    i.e. 'age'         ([underalph][underalphanum]*)",
         '    - a block          i.e. colon followed by newline.',
         '',
         'Notes:',
         ' - in some situations, no attribute key is needed (for feature attributes,',
         '   and when the attribute value starts with some special character that',
         '   uniquely identifies it relative to all other attributes in the',
         '   construct).  Examples:',
         "    - the 'scope' attribute of most constructs is optional because its",
         "      value (a simple or complex block) starts with ':', and amongst all",
         "      block-valued attributes in all constructs, the 'scope' attribute",
         "      is marked as being optional-keyed ... if a ':<nl>' is seen when an",
         "      attribute key is expected, we know to assume 'scope'.  NOTE: We need",
         "      to decide how this interacts with attribute keys whose alias is ':'",
         '      (do we require key/values to appear on the same line???)',
         "    - the 'param' attribute of 'method' has an optional key because its",
         "      value, of type paramlist, starts with '(', and 'param' has been",
         "      marked as optional with '(' as its hint."],
       'raises': {
         'list': [
           {
             'comments': [],
             'first': 'If something',           
             'name': 'Error'},
           {
             'first': 'If something else that extends across multiple lines because',
             'comments': ['the comment is so long.'],
             'name': 'AnotherError'},           
           {
             'comments': [
               'A different variant of a multi-line exception comment that spans multiple',
               'lines so we can test it.'],
             'name': 'AThirdError'},
         ]
       },
       'returns': {
         'comments': [
           '[0] Attribute or None',
           '[1] int (the term code)',
           '  0 = not terminated',
           ' -1 = implicit (no terminator specified, but indentation or eof terminates)',
           "  1 = ';'",
           "  3 = 'end;'",
           "  7 = 'end <cons>;'",
           " 15 = 'end <cons> <id>;'",
           ''],
         'type': 'two-tuple'}},
      # The method has already been fully parsed, so _parseComments() has been
      # invoked and the result assigned to method._comments.
      method.comments())

    class_ = self._module.classNamed('FileParser')
    method = class_.methodNamed('test')
    #pprint.pprint(method.comments())

  def test__parseArgSpec(self):
    self.assertEqual(([], None, None, []), self._method._parseArgSpec(''))

    argspec = self._method._parseArgSpec(
      "a, b, *args, c=0, d='hello, world', e=f(1,g(2)), **kwds")
    self.assertEqual(
      (['a', 'b', 'c', 'd', 'e'], 'args', 'kwds',
       ['0', "'hello, world'", 'f(1,g(2))']),
      argspec)

  def test__parseArgInfo(self):
    self.assertEqual(
      [{'type': 'int',
        'name': 'first_positional_argument',
        'comments': ['A simple int arg.']},
       {'type': 'str',
        'name': 'second_positional_argument',
        'comments': [
          'A simple str arg. This docstr intentionally goes on and on so '
          'that we',
          'can test multi-line docs.']},
       {'default': '1',
        'type': 'int',
        'name': 'first_keyword_argument',
        'comments': [
          'A keyword arg. This docstr has',
          '  indented text',
          'so we can verify it is preserved.']}],
      self._method._parseArgInfo())
    # TODO(wmh): More testing here.

  def test_parseFieldInfo(self):
    method = self._class.methodNamed('__init__')
    self.assertEqual(
      [{'type': 'str',
        'name': 'delim',
        'field': '_delim',
        'comments': [
          'The delimiter between list elements.'],
        'value': 'delim',
       },
       {'type': 'list of str',
        'name': 'data',
        'field': '_data',
        'comments': [
          'The elements of the list.'],
        'value': 'data',
       }],
      method.parseFieldInfo())

  def test__convertDefaultValueToMeta(self):
    method = self._method
    for pyval, metaval in (
      ('True', 'true'),
      ('False', 'false'),
      ('None', 'null'),
      ('3.14', '3.14'),
      ('42', '42'),
      ('"apple"', '"apple"'),
      ("'apple'", "'apple'"),
      ('sys.stdout', '{#sys.stdout#}'),
    ):
      self.assertEqual(metaval, method._convertDefaultValueToMeta(pyval))

  def test_toMeta(self):
    output = python.Output()
    self._method.toMeta(self._metac, output)
    self.maxDiff = None
    self.assertMultiLineEqual(
      '\n'
      'post_extend\n'
      'method instance params:\n'
      '  var first_positional_argument : @int #:\n'
      '    /# A simple int arg.\n'
      '  var second_positional_argument : &!str #:\n'
      '    /# A simple str arg. This docstr intentionally goes on and on so that we\n'
      '    /# can test multi-line docs.\n'
      '  var first_keyword_argument : @int = 1 #:\n'
      '    /# A keyword arg. This docstr has\n'
      '    /#   indented text\n'
      '    /# so we can verify it is preserved.\n'
      'comment:\n'
      '  /# Instance method.\n'
      'scope:\n'
      '  if first_keyword_argument == 1:\n'
      '    return first_positional_argument\n'
      '  else:\n'
      '    return second_positional_argument\n'
      'test:\n'
      '  /# Here in test.\n'
      '  a = 1\n'
      'end method instance;',
      '\n'.join(output.lines()))

    output = python.Output()
    self._method2.toMeta(self._metac, output)
    self.assertMultiLineEqual(
      '\n'
      'static\n'
      'method Static params:\n'
      '  var a : @int #:\n'
      '    /# A simple int arg.\n'
      'comment:\n'
      '  /# Static method.\n'
      'scope:\n'
      '  /# first some basic code\n'
      '  a *= 2\n'
      '  /# Now a heredoc\n'
      '  val = """\n'
      '     >|this is a test to see how\n'
      '     >|heredoc notation is parsed.\n'
      '     >|"""\n'
      '  /# And now some more basic code\n'
      '  print val\n'
      '  return a\n'
      'test:\n'
      '  self.assertEqual(4, kernel.A.Static(2))\n'
      'end method Static;',
      '\n'.join(output.lines()))

    self.assertMultiLineEqual(
      '\n'
      '    method rootDir comment:\n'
      '      /# Obtain the root directory for Meta.\n'
      '    scope:\n'
      "      rootdir = os.getenv('META3_ROOT')\n"
      '      if not os.path.exists(rootdir):\n'
      "        raise Error('Failed to find META3_ROOT %s' % rootdir)\n"
      '      return rootdir\n'
      '    test:\n'
      '      self.assertEqual(\n'
      "        os.getenv('META3_ROOT'), self._meta.rootDir())\n"
      '    end method rootDir;\n',
      self.convertGivenMethod('parser.Meta.rootDir'))

    self.assertMultiLineEqual(
      '\n'
      '    meta\n'
      '    method StartsList params:\n'
      '      var val : &!str #:\n'
      '        /# The value to analyze.\n'
      '    comment:\n'
      '      /# Determine if the given value represents a literal list.\n'  
      '    scope:\n'
      '      /# TODO(wmh): Need a much more efficient implementation - '
      'this is way\n'
      '      /# to expensive for something called very very often.\n'
      "      return val.startswith('List') and len(val) > 4 and "
      'not val[4].isalnum()\n'
      '    test:\n'
      "      self.assertTrue(parser.LiteralList.StartsList('List|'))\n"
      "      self.assertTrue(parser.LiteralList.StartsList('List<'))\n"
      "      self.assertTrue(parser.LiteralList.StartsList('List,'))\n"
      "      self.assertFalse(parser.LiteralList.StartsList('List'))\n"
      "      self.assertFalse(parser.LiteralList.StartsList('Lisf'))\n"
      '    end method StartsList;\n',
      self.convertGivenMethod('parser.LiteralList.StartsList'))

    #print self.convertGivenMethod('parser.FileParser.parseAttribute')


if __name__ == '__main__':
  logging.basicConfig(level=logging.INFO)
  testing.main()
